/*
 Copyright 2006-2011 Abdulla Abdurakhmanov (abdulla@latestbit.com)
 Original sources are available at www.latestbit.com

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

package org.bn.types;

import com.sun.org.apache.xalan.internal.xsltc.dom.BitArray;
import java.util.Arrays;
import org.bn.coders.CoderUtils;
import org.bn.utils.BitArrayOutputStream;

/**
 * BitString represents ASN.1 BIT STRING data types
 * @todo Need more functional operations for write/read bits
 */
public class BitString {
    private byte[] bitStrValue = new byte[0];
    private int trailBitsCnt = 0; // count of buffer bit's trail
    
    public BitString() {
    }

    /**
     *
     * @param bString String in the format of the default values notation: 
     * 'bbb...bb'B for binary string
     * 'hhh...hh'H for hexadecimal string
     */
    public BitString(String bString) {
        this( (bString.toUpperCase().endsWith("H") || (bString.toUpperCase().endsWith("B")))?
                CoderUtils.defStringToOctetString(bString):parseBinaryString(bString));
            
    }
    public BitString(byte[] bitStrValue) {
        setValue(bitStrValue);
    }

    public BitString(byte[] bitStrValue,int trailBitsCnt) {
        setValue(bitStrValue, trailBitsCnt);
    }
    
    public BitString(BitString src) {
        setValue(src.getValue(),src.getTrailBitsCnt());        
    }
    
    public int getLength() {
        return bitStrValue.length;
    }
    
    public int getTrailBitsCnt() {
        return trailBitsCnt;
    }

    public int getLengthInBits() {
        if (trailBitsCnt == 0)
            return getLength()*8;
	return Math.max(0,getLength()-1)*8 + getTrailBitsCnt();
    }
    
    public byte[] getValue() {
        return bitStrValue;
    }
    public byte[] getValueWithoutTrailBits() {
        if (getLength() ==0)
            return new byte[0];
        byte[] bitStrValueWithoutTrailBist = new byte[getLength()-1];
        System.arraycopy(bitStrValue, 0, bitStrValueWithoutTrailBist, 0, bitStrValueWithoutTrailBist.length);
        return bitStrValueWithoutTrailBist;
    }
    
    public void setValue(byte[] val) {
        setValue(val,0);
    }
    
    public void setValue(byte[] val, int trailBitsCnt) {
        if ((val.length == 0) && (trailBitsCnt != 0))
                throw new IllegalArgumentException("trailing bits count must be zero in zero-length byte array");
        this.bitStrValue = val;
        this.trailBitsCnt = trailBitsCnt;
    }    
    
    public BitString trimTrailBits() {
        return new BitString(getValueWithoutTrailBits());
    }
    public BitString trimAllExceptTrailBits() {
        if (getLength() == 0) 
            return new BitString();
        return new BitString(new byte[] {bitStrValue[getLength()-1]}, trailBitsCnt);
    }
    
    public int bitAt(int index) {
        if ((getLength() == 0) || index >= getLengthInBits())
            throw new IndexOutOfBoundsException();
        int posOctet = index / 8;
        int posBit = index % 8;
        return ((bitStrValue[posOctet] >> (7-posBit)) & 0x1);
    }

    public boolean startsWith(BitString prefix) {
        if (prefix.getLength() == 0)
            return true;
        if (prefix.getLengthInBits() > getLengthInBits())
            return false;
       
        int prefixoctetslen = prefix.getLengthInBits()/8;
        for ( int i = 0; i < prefixoctetslen; i++)
            if (bitStrValue[i] != prefix.bitStrValue[i])
                return false;
        if (prefix.trailBitsCnt > 0) {
            byte lastOctet = (byte)((~0 << 8-prefix.trailBitsCnt) & prefix.bitStrValue[prefix.getLength()-1]);
            if (lastOctet != ((~0 << 8-prefix.trailBitsCnt) & bitStrValue[prefix.getLength()-1]))
                return false;
        }
        return true;
    }
    
    public boolean endsWith(BitString suffix) {
        if (suffix.getLength() == 0)
            return true;
        if (suffix.getLengthInBits() > getLengthInBits())
            return false;
        int leninBits = getLengthInBits();
        int lenSuffixInBits = suffix.getLengthInBits();
        for (int i = 0; i < lenSuffixInBits; i++)
            if (suffix.bitAt(i) != bitAt(leninBits-lenSuffixInBits+i))
                return false;
        return true;
    }
    
    /**
     *
     * @param index beginning index
     * @param len maximum length. Could be less is the end of BitString is reached.
     * @return the subBitString
     */
    public BitString subBitString(int index, int len) {
        BitArrayOutputStream baos = new BitArrayOutputStream();
        for (int i = 0; i < len; i++)
            try {
                baos.writeBit(bitAt(i+index));
            } catch (IndexOutOfBoundsException e) {
                break;
            }
        return baos.getBitString();
    }
    
    public BitString subBitString(int index) {
        return subBitString(index, getLengthInBits());
    }
    @Override
    public boolean equals(Object o) {
        if (!( o instanceof BitString))
            return false;
        BitString obs = (BitString) o;
        return Arrays.equals(bitStrValue, obs.bitStrValue);
    }
    
    
    /** Returns a string containing '0' and '1' chars that represents the bit string.
     * 
     * This method along with the String param constructor, can be used manipulate Bit String,
     * concatenate, substrings, replace, etc.
     *
     * @return string containing '0' and '1' chars that represents the bit string
     */
    @Override
    public String toString() {
        if (bitStrValue.length == 0)
            return "";
        StringBuffer sb = new StringBuffer(getLengthInBits());
        int len = (trailBitsCnt>0)?bitStrValue.length-1:bitStrValue.length;
        for (int i = 0; i < len; i++) {
            for (int j = 7; j >= 0; j--) {
                sb.append((( (bitStrValue[i] >> j) & 0x1) != 0)?'1':'0');
            }
        }
        for (int j = 1; j<=trailBitsCnt; j++)
            sb.append((( (bitStrValue[bitStrValue.length-1] >> 8-j) & 0x1) != 0)?'1':'0');
        return sb.toString();
    }
    
    
    public static BitString parseBinaryString(String bhString) {        
        boolean hasTrailBits = bhString.length()%8!=0;
        int trailBits = 0;
        byte[] resultBuf = new byte[bhString.length()/8 + (hasTrailBits?1:0)];
        int currentStrPos = 0;
        for(int i=0;i<resultBuf.length;i++) {
            byte bt = 0x00;
            int bitCnt = currentStrPos;
            while(bitCnt<currentStrPos+8 && bitCnt< bhString.length()) {
                if(bhString.charAt(bitCnt)!='0')
                    bt |=  ( 0x01 << (7- (bitCnt-currentStrPos)));
                bitCnt++;
            }
            currentStrPos+=8;            
            if(bitCnt!=currentStrPos)
                trailBits = 8 - (currentStrPos - bitCnt);
            // hi byte
            resultBuf[i] = bt;
        }
        BitString result = new BitString (resultBuf,trailBits);        
        return result;
    }
    
    public static BitString parseHexadecimalString(String bhString) {
           boolean hasTrailBits = bhString.length()%2!=0;
           BitString result = new BitString (new byte[bhString.length()/2 + (hasTrailBits ? 1:0)], hasTrailBits? 4:0);
           final byte hex[] = {0, 1, 2,3, 4, 5, 6, 7, 8, 9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA, 0xB, 0xC, 0xD, 0xE,0xF};
           
           for(int i=0;i<result.getLength();i++) {
               // high byte
               result.getValue()[i] = (byte)(hex[((int)(bhString.charAt(i*2)) - 0x30)] << 4);
               if(!hasTrailBits || (hasTrailBits && i<result.getLength()-1))                
                result.getValue()[i] |= (byte)(hex[((int)(bhString.charAt(i*2+1)) - 0x30)] & 0x0F);
           }
           return result;
    }    
}
